@@ -0,0 +1,39 @@ |
||
1 |
+module JsonPathOptionsOverwritable |
|
2 |
+ extend ActiveSupport::Concern |
|
3 |
+ # Using this concern allows providing optional `<attribute>_path` options hash |
|
4 |
+ # attributes which will then (if not blank) be interpolated using the provided JSONPath. |
|
5 |
+ # |
|
6 |
+ # Example options Hash: |
|
7 |
+ # { |
|
8 |
+ # name: 'Huginn', |
|
9 |
+ # name_path: '$.name', |
|
10 |
+ # title: 'Hello from Huginn' |
|
11 |
+ # title_path: '' |
|
12 |
+ # } |
|
13 |
+ # Example event payload: |
|
14 |
+ # { |
|
15 |
+ # name: 'dynamic huginn' |
|
16 |
+ # } |
|
17 |
+ # calling agent.merge_json_path_options(event) returns the following hash: |
|
18 |
+ # { |
|
19 |
+ # name: 'dynamic huginn' |
|
20 |
+ # title: 'Hello from Huginn' |
|
21 |
+ # } |
|
22 |
+ |
|
23 |
+ private |
|
24 |
+ def merge_json_path_options(event) |
|
25 |
+ options.select { |k, v| options_with_path.include? k}.tap do |merged_options| |
|
26 |
+ options_with_path.each do |a| |
|
27 |
+ merged_options[a] = select_option(event, a) |
|
28 |
+ end |
|
29 |
+ end |
|
30 |
+ end |
|
31 |
+ |
|
32 |
+ def select_option(event, a) |
|
33 |
+ if options[a.to_s + '_path'].present? |
|
34 |
+ Utils.value_at(event.payload, options[a.to_s + '_path']) |
|
35 |
+ else |
|
36 |
+ options[a] |
|
37 |
+ end |
|
38 |
+ end |
|
39 |
+end |
@@ -0,0 +1,15 @@ |
||
1 |
+module WorkingHelpers |
|
2 |
+ extend ActiveSupport::Concern |
|
3 |
+ |
|
4 |
+ def event_created_within?(days) |
|
5 |
+ last_event_at && last_event_at > days.to_i.days.ago |
|
6 |
+ end |
|
7 |
+ |
|
8 |
+ def recent_error_logs? |
|
9 |
+ last_event_at && last_error_log_at && last_error_log_at > (last_event_at - 2.minutes) |
|
10 |
+ end |
|
11 |
+ |
|
12 |
+ def received_event_without_error? |
|
13 |
+ (last_receive_at.present? && last_error_log_at.blank?) || (last_receive_at.present? && last_error_log_at.present? && last_receive_at > last_error_log_at) |
|
14 |
+ end |
|
15 |
+end |
@@ -11,6 +11,7 @@ class Agent < ActiveRecord::Base |
||
11 | 11 |
include MarkdownClassAttributes |
12 | 12 |
include JSONSerializedField |
13 | 13 |
include RDBMSFunctions |
14 |
+ include WorkingHelpers |
|
14 | 15 |
|
15 | 16 |
markdown_class_attributes :description, :event_description |
16 | 17 |
|
@@ -83,14 +84,6 @@ class Agent < ActiveRecord::Base |
||
83 | 84 |
raise "Implement me in your subclass" |
84 | 85 |
end |
85 | 86 |
|
86 |
- def event_created_within?(days) |
|
87 |
- last_event_at && last_event_at > days.to_i.days.ago |
|
88 |
- end |
|
89 |
- |
|
90 |
- def recent_error_logs? |
|
91 |
- last_event_at && last_error_log_at && last_error_log_at > (last_event_at - 2.minutes) |
|
92 |
- end |
|
93 |
- |
|
94 | 87 |
def create_event(attrs) |
95 | 88 |
if can_create_events? |
96 | 89 |
events.create!({ |
@@ -1,5 +1,7 @@ |
||
1 | 1 |
module Agents |
2 | 2 |
class HipchatAgent < Agent |
3 |
+ include JsonPathOptionsOverwritable |
|
4 |
+ |
|
3 | 5 |
cannot_be_scheduled! |
4 | 6 |
cannot_create_events! |
5 | 7 |
|
@@ -47,30 +49,14 @@ module Agents |
||
47 | 49 |
def receive(incoming_events) |
48 | 50 |
client = HipChat::Client.new(options[:auth_token]) |
49 | 51 |
incoming_events.each do |event| |
50 |
- mo = merge_options event |
|
52 |
+ mo = merge_json_path_options event |
|
51 | 53 |
client[mo[:room_name]].send(mo[:username], mo[:message], :notify => mo[:notify].to_s == 'true' ? 1 : 0, :color => mo[:color]) |
52 | 54 |
end |
53 | 55 |
end |
54 | 56 |
|
55 | 57 |
private |
56 |
- def select_option(event, a) |
|
57 |
- if options[a.to_s + '_path'].present? |
|
58 |
- Utils.value_at(event.payload, options[a.to_s + '_path']) |
|
59 |
- else |
|
60 |
- options[a] |
|
61 |
- end |
|
62 |
- end |
|
63 |
- |
|
64 | 58 |
def options_with_path |
65 | 59 |
[:room_name, :username, :message, :notify, :color] |
66 | 60 |
end |
67 |
- |
|
68 |
- def merge_options event |
|
69 |
- options.select { |k, v| options_with_path.include? k}.tap do |merged_options| |
|
70 |
- options_with_path.each do |a| |
|
71 |
- merged_options[a] = select_option(event, a) |
|
72 |
- end |
|
73 |
- end |
|
74 |
- end |
|
75 | 61 |
end |
76 | 62 |
end |
@@ -0,0 +1,67 @@ |
||
1 |
+module Agents |
|
2 |
+ class PushbulletAgent < Agent |
|
3 |
+ include JsonPathOptionsOverwritable |
|
4 |
+ |
|
5 |
+ cannot_be_scheduled! |
|
6 |
+ cannot_create_events! |
|
7 |
+ |
|
8 |
+ description <<-MD |
|
9 |
+ The Pushbullet agent sends pushes to a pushbullet device |
|
10 |
+ |
|
11 |
+ To authenticate you need to set the `api_key`, you can find yours at your account page: |
|
12 |
+ |
|
13 |
+ `https://www.pushbullet.com/account` |
|
14 |
+ |
|
15 |
+ Currently you need to get a the device identification manually: |
|
16 |
+ |
|
17 |
+ `curl -u <your api key here>: https://api.pushbullet.com/api/devices` |
|
18 |
+ |
|
19 |
+ Put one of the retured `iden` strings into the `device_id` field. |
|
20 |
+ |
|
21 |
+ You can provide a `title` and a `body`. |
|
22 |
+ |
|
23 |
+ If you want to specify `title` or `body` per event, you can provide a [JSONPath](http://goessner.net/articles/JsonPath/) for each of them. |
|
24 |
+ MD |
|
25 |
+ |
|
26 |
+ def default_options |
|
27 |
+ { |
|
28 |
+ 'api_key' => '', |
|
29 |
+ 'device_id' => '', |
|
30 |
+ 'title' => "Hello from Huginn!", |
|
31 |
+ 'title_path' => '', |
|
32 |
+ 'body' => '', |
|
33 |
+ 'body_path' => '', |
|
34 |
+ } |
|
35 |
+ end |
|
36 |
+ |
|
37 |
+ def validate_options |
|
38 |
+ errors.add(:base, "you need to specify a pushbullet api_key") unless options['api_key'].present? |
|
39 |
+ errors.add(:base, "you need to specify a device_id") if options['device_id'].blank? |
|
40 |
+ end |
|
41 |
+ |
|
42 |
+ def working? |
|
43 |
+ received_event_without_error? |
|
44 |
+ end |
|
45 |
+ |
|
46 |
+ def receive(incoming_events) |
|
47 |
+ incoming_events.each do |event| |
|
48 |
+ response = HTTParty.post "https://api.pushbullet.com/api/pushes", query_options(event) |
|
49 |
+ error(response.body) if response.body.include? 'error' |
|
50 |
+ end |
|
51 |
+ end |
|
52 |
+ |
|
53 |
+ private |
|
54 |
+ def query_options(event) |
|
55 |
+ mo = merge_json_path_options event |
|
56 |
+ basic_options.deep_merge(:body => {:title => mo[:title], :body => mo[:body]}) |
|
57 |
+ end |
|
58 |
+ |
|
59 |
+ def basic_options |
|
60 |
+ {:basic_auth => {:username =>options[:api_key], :password=>''}, :body => {:device_iden => options[:device_id], :type => 'note'}} |
|
61 |
+ end |
|
62 |
+ |
|
63 |
+ def options_with_path |
|
64 |
+ [:title, :body] |
|
65 |
+ end |
|
66 |
+ end |
|
67 |
+end |
@@ -1,6 +1,9 @@ |
||
1 | 1 |
require 'spec_helper' |
2 |
+require 'models/concerns/working_helpers' |
|
2 | 3 |
|
3 | 4 |
describe Agent do |
5 |
+ it_behaves_like WorkingHelpers |
|
6 |
+ |
|
4 | 7 |
describe ".run_schedule" do |
5 | 8 |
before do |
6 | 9 |
Agents::WeatherAgent.count.should > 0 |
@@ -610,32 +613,6 @@ describe Agent do |
||
610 | 613 |
end |
611 | 614 |
end |
612 | 615 |
|
613 |
- describe "recent_error_logs?" do |
|
614 |
- it "returns true if last_error_log_at is near last_event_at" do |
|
615 |
- agent = Agent.new |
|
616 |
- |
|
617 |
- agent.last_error_log_at = 10.minutes.ago |
|
618 |
- agent.last_event_at = 10.minutes.ago |
|
619 |
- agent.recent_error_logs?.should be_true |
|
620 |
- |
|
621 |
- agent.last_error_log_at = 11.minutes.ago |
|
622 |
- agent.last_event_at = 10.minutes.ago |
|
623 |
- agent.recent_error_logs?.should be_true |
|
624 |
- |
|
625 |
- agent.last_error_log_at = 5.minutes.ago |
|
626 |
- agent.last_event_at = 10.minutes.ago |
|
627 |
- agent.recent_error_logs?.should be_true |
|
628 |
- |
|
629 |
- agent.last_error_log_at = 15.minutes.ago |
|
630 |
- agent.last_event_at = 10.minutes.ago |
|
631 |
- agent.recent_error_logs?.should be_false |
|
632 |
- |
|
633 |
- agent.last_error_log_at = 2.days.ago |
|
634 |
- agent.last_event_at = 10.minutes.ago |
|
635 |
- agent.recent_error_logs?.should be_false |
|
636 |
- end |
|
637 |
- end |
|
638 |
- |
|
639 | 616 |
describe "scopes" do |
640 | 617 |
describe "of_type" do |
641 | 618 |
it "should accept classes" do |
@@ -1,6 +1,9 @@ |
||
1 | 1 |
require 'spec_helper' |
2 |
+require 'models/concerns/json_path_options_overwritable' |
|
2 | 3 |
|
3 | 4 |
describe Agents::HipchatAgent do |
5 |
+ it_behaves_like JsonPathOptionsOverwritable |
|
6 |
+ |
|
4 | 7 |
before(:each) do |
5 | 8 |
@valid_params = { |
6 | 9 |
'auth_token' => 'token', |
@@ -49,29 +52,6 @@ describe Agents::HipchatAgent do |
||
49 | 52 |
|
50 | 53 |
end |
51 | 54 |
|
52 |
- describe "helpers" do |
|
53 |
- describe "select_option" do |
|
54 |
- it "should use the room_name_path if specified" do |
|
55 |
- @checker.options['room_name_path'] = "$.room_name" |
|
56 |
- @checker.send(:select_option, @event, :room_name).should == "test room" |
|
57 |
- end |
|
58 |
- |
|
59 |
- it "should use the normal option when the path option is blank" do |
|
60 |
- @checker.send(:select_option, @event, :room_name).should == "test" |
|
61 |
- end |
|
62 |
- end |
|
63 |
- |
|
64 |
- it "should merge all options" do |
|
65 |
- @checker.send(:merge_options, @event).deep_symbolize_keys.should == { |
|
66 |
- :room_name => "test", |
|
67 |
- :username => "Huggin user", |
|
68 |
- :message => "Looks like its going to rain", |
|
69 |
- :notify => false, |
|
70 |
- :color => "yellow" |
|
71 |
- } |
|
72 |
- end |
|
73 |
- end |
|
74 |
- |
|
75 | 55 |
describe "#receive" do |
76 | 56 |
it "send a message to the hipchat" do |
77 | 57 |
any_instance_of(HipChat::Room) do |obj| |
@@ -0,0 +1,80 @@ |
||
1 |
+require 'spec_helper' |
|
2 |
+require 'models/concerns/json_path_options_overwritable' |
|
3 |
+ |
|
4 |
+describe Agents::PushbulletAgent do |
|
5 |
+ it_behaves_like JsonPathOptionsOverwritable |
|
6 |
+ |
|
7 |
+ before(:each) do |
|
8 |
+ @valid_params = { |
|
9 |
+ 'api_key' => 'token', |
|
10 |
+ 'device_id' => '124', |
|
11 |
+ 'body_path' => '$.body', |
|
12 |
+ 'title' => 'hello from huginn' |
|
13 |
+ } |
|
14 |
+ |
|
15 |
+ @checker = Agents::PushbulletAgent.new(:name => "somename", :options => @valid_params) |
|
16 |
+ @checker.user = users(:jane) |
|
17 |
+ @checker.save! |
|
18 |
+ |
|
19 |
+ @event = Event.new |
|
20 |
+ @event.agent = agents(:bob_weather_agent) |
|
21 |
+ @event.payload = { :body => 'One two test' } |
|
22 |
+ @event.save! |
|
23 |
+ end |
|
24 |
+ |
|
25 |
+ describe "validating" do |
|
26 |
+ before do |
|
27 |
+ @checker.should be_valid |
|
28 |
+ end |
|
29 |
+ |
|
30 |
+ it "should require the api_key" do |
|
31 |
+ @checker.options['api_key'] = nil |
|
32 |
+ @checker.should_not be_valid |
|
33 |
+ end |
|
34 |
+ |
|
35 |
+ it "should require the device_id" do |
|
36 |
+ @checker.options['device_id'] = nil |
|
37 |
+ @checker.should_not be_valid |
|
38 |
+ end |
|
39 |
+ end |
|
40 |
+ |
|
41 |
+ describe "helpers" do |
|
42 |
+ it "it should return the correct basic_options" do |
|
43 |
+ @checker.send(:basic_options).should == {:basic_auth => {:username =>@checker.options[:api_key], :password=>''}, |
|
44 |
+ :body => {:device_iden => @checker.options[:device_id], :type => 'note'}} |
|
45 |
+ end |
|
46 |
+ |
|
47 |
+ |
|
48 |
+ it "should return the query_options" do |
|
49 |
+ @checker.send(:query_options, @event).should == @checker.send(:basic_options).deep_merge({ |
|
50 |
+ :body => {:title => 'hello from huginn', :body => 'One two test'} |
|
51 |
+ }) |
|
52 |
+ end |
|
53 |
+ end |
|
54 |
+ |
|
55 |
+ describe "#receive" do |
|
56 |
+ it "send a message to the hipchat" do |
|
57 |
+ stub_request(:post, "https://token:@api.pushbullet.com/api/pushes"). |
|
58 |
+ with(:body => "device_iden=124&type=note&title=hello%20from%20huginn&body=One%20two%20test"). |
|
59 |
+ to_return(:status => 200, :body => "ok", :headers => {}) |
|
60 |
+ dont_allow(@checker).error |
|
61 |
+ @checker.receive([@event]) |
|
62 |
+ end |
|
63 |
+ |
|
64 |
+ it "should log resquests which return an error" do |
|
65 |
+ stub_request(:post, "https://token:@api.pushbullet.com/api/pushes"). |
|
66 |
+ with(:body => "device_iden=124&type=note&title=hello%20from%20huginn&body=One%20two%20test"). |
|
67 |
+ to_return(:status => 200, :body => "error", :headers => {}) |
|
68 |
+ mock(@checker).error("error") |
|
69 |
+ @checker.receive([@event]) |
|
70 |
+ end |
|
71 |
+ end |
|
72 |
+ |
|
73 |
+ describe "#working?" do |
|
74 |
+ it "should not be working until the first event was received" do |
|
75 |
+ @checker.should_not be_working |
|
76 |
+ @checker.last_receive_at = Time.now |
|
77 |
+ @checker.should be_working |
|
78 |
+ end |
|
79 |
+ end |
|
80 |
+end |
@@ -0,0 +1,31 @@ |
||
1 |
+require 'spec_helper' |
|
2 |
+ |
|
3 |
+shared_examples_for JsonPathOptionsOverwritable do |
|
4 |
+ before(:each) do |
|
5 |
+ @valid_params = described_class.new.default_options |
|
6 |
+ |
|
7 |
+ @checker = described_class.new(:name => "somename", :options => @valid_params) |
|
8 |
+ @checker.user = users(:jane) |
|
9 |
+ |
|
10 |
+ @event = Event.new |
|
11 |
+ @event.agent = agents(:bob_weather_agent) |
|
12 |
+ @event.payload = { :room_name => 'test room', :message => 'Looks like its going to rain', username: "Huggin user"} |
|
13 |
+ @event.save! |
|
14 |
+ end |
|
15 |
+ |
|
16 |
+ describe "select_option" do |
|
17 |
+ it "should use the room_name_path if specified" do |
|
18 |
+ @checker.options['room_name_path'] = "$.room_name" |
|
19 |
+ @checker.send(:select_option, @event, :room_name).should == "test room" |
|
20 |
+ end |
|
21 |
+ |
|
22 |
+ it "should use the normal option when the path option is blank" do |
|
23 |
+ @checker.options['room_name'] = 'test' |
|
24 |
+ @checker.send(:select_option, @event, :room_name).should == "test" |
|
25 |
+ end |
|
26 |
+ end |
|
27 |
+ |
|
28 |
+ it "should merge all options" do |
|
29 |
+ @checker.send(:merge_json_path_options, @event).symbolize_keys.keys.should == @checker.send(:options_with_path) |
|
30 |
+ end |
|
31 |
+end |
@@ -0,0 +1,53 @@ |
||
1 |
+require 'spec_helper' |
|
2 |
+ |
|
3 |
+shared_examples_for WorkingHelpers do |
|
4 |
+ describe "recent_error_logs?" do |
|
5 |
+ it "returns true if last_error_log_at is near last_event_at" do |
|
6 |
+ agent = Agent.new |
|
7 |
+ |
|
8 |
+ agent.last_error_log_at = 10.minutes.ago |
|
9 |
+ agent.last_event_at = 10.minutes.ago |
|
10 |
+ agent.recent_error_logs?.should be_true |
|
11 |
+ |
|
12 |
+ agent.last_error_log_at = 11.minutes.ago |
|
13 |
+ agent.last_event_at = 10.minutes.ago |
|
14 |
+ agent.recent_error_logs?.should be_true |
|
15 |
+ |
|
16 |
+ agent.last_error_log_at = 5.minutes.ago |
|
17 |
+ agent.last_event_at = 10.minutes.ago |
|
18 |
+ agent.recent_error_logs?.should be_true |
|
19 |
+ |
|
20 |
+ agent.last_error_log_at = 15.minutes.ago |
|
21 |
+ agent.last_event_at = 10.minutes.ago |
|
22 |
+ agent.recent_error_logs?.should be_false |
|
23 |
+ |
|
24 |
+ agent.last_error_log_at = 2.days.ago |
|
25 |
+ agent.last_event_at = 10.minutes.ago |
|
26 |
+ agent.recent_error_logs?.should be_false |
|
27 |
+ end |
|
28 |
+ end |
|
29 |
+ describe "received_event_without_error?" do |
|
30 |
+ before do |
|
31 |
+ @agent = Agent.new |
|
32 |
+ end |
|
33 |
+ |
|
34 |
+ it "should return false until the first event was received" do |
|
35 |
+ @agent.received_event_without_error?.should == false |
|
36 |
+ @agent.last_receive_at = Time.now |
|
37 |
+ @agent.received_event_without_error?.should == true |
|
38 |
+ end |
|
39 |
+ |
|
40 |
+ it "should return false when the last error occured after the last received event" do |
|
41 |
+ @agent.last_receive_at = Time.now - 1.minute |
|
42 |
+ @agent.last_error_log_at = Time.now |
|
43 |
+ @agent.received_event_without_error?.should == false |
|
44 |
+ end |
|
45 |
+ |
|
46 |
+ it "should return true when the last received event occured after the last error" do |
|
47 |
+ @agent.last_receive_at = Time.now |
|
48 |
+ @agent.last_error_log_at = Time.now - 1.minute |
|
49 |
+ @agent.received_event_without_error?.should == true |
|
50 |
+ end |
|
51 |
+ end |
|
52 |
+ |
|
53 |
+end |
@@ -1,8 +1,13 @@ |
||
1 | 1 |
# This file is copied to spec/ when you run 'rails generate rspec:install' |
2 | 2 |
ENV["RAILS_ENV"] ||= 'test' |
3 | 3 |
|
4 |
-require 'coveralls' |
|
5 |
-Coveralls.wear!('rails') |
|
4 |
+if ENV['COVERAGE'] |
|
5 |
+ require 'simplecov' |
|
6 |
+ SimpleCov.start 'rails' |
|
7 |
+else |
|
8 |
+ require 'coveralls' |
|
9 |
+ Coveralls.wear!('rails') |
|
10 |
+end |
|
6 | 11 |
|
7 | 12 |
require File.expand_path("../../config/environment", __FILE__) |
8 | 13 |
require 'rspec/rails' |